Conversation
sidcha
force-pushed
the
trs_dev
branch
4 times, most recently
from
July 17, 2026 15:43
e6e5c66 to
3da491c
Compare
TRS tunnels raw smart-card APDUs from the CP, through a PD acting as a transparent pipe, to a card in the reader (OSDP CMD_XWR/REPLY_XRD, capability SMART_CARD_SUPPORT). The control panel drives the card directly, so a compromised reader cannot forge a credential. The CP runs the session as a library-driven state machine: it negotiates transparent mode, streams the app's APDUs, and tears the session down on osdp_cp_trs_stop(). Apps submit OSDP_CMD_XWR commands and receive card responses as OSDP_EVENT_TRS events. The PD answers each APDU either synchronously, in the command callback, or deferred: it ACKs "working" and delivers the response on a later poll once a slow card responds. Deferral keeps a shared multi-drop bus non-blocking, so each PD's session makes progress independently. A Card Present reply is accepted with or without the trailing status byte -- the spec marks it optional (v2.2 section 7.26.8) and readers like the HID RPK40 omit it; the status then reports as unspecified. Gated behind OPT_BUILD_OSDP_TRS (default off); wired into both build systems and covered by a CP<->PD APDU round-trip unit test. Fixes: #22 Co-developed-by: Aaron Tulino (Aaronjamt) <git@aaronjamt.com> Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
Table 36 marks the mode-config byte optional: in its absence 0x00 is used. The PD decoder demanded both mode and config, NAK-ing a 3-byte Mode-Set that a foreign ACU may legitimately send. Related-to: #22 Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
The RPK40 NAKs a CARD_SCAN it does not implement with MSG_CHK, which the CP's corrupted-frame heuristic answered with up to eight blind retransmissions before tearing the card session down. Replaying card traffic can have card-side effects (PIN retry counters, transaction state), so CMD_XWR is exempt from the resend heuristic entirely; and since a reader may pick any NAK code it fancies for an unimplemented card command, every NAK to an app-owned card command is a soft failure -- one failed command, session intact -- unless the code says the link itself is broken (SEQ_NUM, SC_UNSUP, SC_COND). The test runs on a plaintext link, as in the field trace: the mock channel rewrites the reader's reply into a NAK(MSG_CHK) at the same sequence number, which is also why the mock gained a no-SCBK device setup -- rewriting a secured reply would desync the MAC chain, which a real quirky reader never does. Related-to: #22 Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
A reader left in transparent mode -- failed teardown, CP restart -- keeps answering polls with XRD replies. The CP treated those as unknown responses and cycled the PD offline for the full retry penalty, taking a healthy link down over a stale reader mode. Swallow the reply with a warning instead: it is well-formed, it just has no session to route to. The payload is dropped, no event reaches the app, and the link stays up. Related-to: #22 Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
The 64-byte APDU carrier could not hold what the headline use case moves: PIV certificate reads chain GET RESPONSE chunks up to the short-APDU maximum of 258 bytes (255 data + SW1SW2). Raise the default to 258 behind an #ifndef so constrained builds can dial it back down -- osdp_cmd/osdp_event embed a buffer of this size, so the override is also the memory knob. The usable size is further bound by the negotiated packet size; a C-APDU that cannot fit it is now rejected at submit time instead of failing mid-band. Full-size chunks additionally need -DOSDP_PACKET_BUF_SIZE=512 (default 256). Chunking inside the library would be wrong here: TRS is a transparent tunnel and ISO 7816-4 has its own chaining. Related-to: #22 Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
The card (emu-card.c, libosdp-free) speaks just enough ISO 7816-4 to model the headline workflow: SELECT of the PIV AID, GET DATA of a 1.2KB certificate-sized object, and 61xx/GET RESPONSE chaining. The scenarios drive it through a real CP<->PD session over the mock channel, modeled on the reader behaviour reported in issue #22: a full chained certificate read, a session that must ride out a busy spell, a permanently-busy reader costing one soft-failed command, and a card leaving the field mid-band with no card-not-present courtesy from the reader. Related-to: #22 Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
The channel-hook frame parsers assumed the leading mark byte from build options, but its presence also varies by direction and RX mode; detect it from the frame instead. Also add test-pd-zc.c to the lean build's test sources -- the zero-copy unit-test binary never linked there. Related-to: #22 Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
A reader may announce a card in its field as an XRD poll response with no TRS session open (v2.2 sections 5.11, 7.26.2, 7.26.8). Forward Mode-0 card-info and Mode-1 card-present reports to the app as OSDP_EVENT_TRS instead of discarding them; every other out-of-session XRD is still shrugged off without cycling the PD offline. Related-to: #22 Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
Some readers (HID RPK40) stop reporting ordinary credentials while in transparent mode and give no smart-card sighting in their default mode, so neither mode alone can read both credential types. Add an opt-in, library-managed scan (osdp_cp_trs_scan_enable) that time-slices the reader: dwell in the default mode, hold transparent mode briefly watching for a sighting, repeat. A sighting is forwarded as an OSDP_EVENT_TRS event and transparent mode is held so the band the app opens in response adopts the reader without renegotiating the mode. The probe rides the existing TRS session machinery; pd->trs.probe only suppresses session notifications and band flushing. Ordinary card or keypad activity restarts the default-mode dwell so a probe never cuts into an in-progress read. A refused probe raises OSDP_TRS_SCAN_SUSPENDED and retries with exponential backoff. Related-to: #22 Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
A failed mode-set does not prove the reader stayed in its default mode: it may have entered transparent mode and had its reply lost or mangled on the wire, and a reader stuck in mode 01 stops reporting ordinary card reads. Mode-set(00) is idempotent, so always spend one teardown round trip instead of assuming there is nothing to undo. Related-to: #22 Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
Scan cadence (silent mode flips, no notification/completion spam), mode-0 activity deferring probes, disable/flush mid-probe, sighting hold with band adoption (exactly one mode-set(01) on the wire from enable to band close), refused-probe backoff with recovery, and an emulated RPK40 workflow: standard read, smart-card sighting during a probe, full chained PIV certificate read over the adopted session, standard read again. Related-to: #22 Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
hold_ms is documented as the window a probe keeps transparent mode open for the app to answer a sighting with a START, but every sighting also refreshed the deadline it is measured from. A reader is free to report a card on every poll (v2.2 section 7.26.8), so a card left sitting in the field renewed the hold indefinitely and kept the reader in mode 01, which is precisely the state that stops ordinary credential reads. Stamp the deadline on the first sighting only. Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
The presence scan only ever waited for a reader to volunteer a card report on a poll. Readers are free to do that, but they are equally free to latch a sighting for the rest of a transparent session, and several do -- so a card lifted out of the field was never reported as gone and an application had no way to find out short of sending card traffic of its own. The spec already has the question we were not asking: the mode-01 card scan (v2.2 section 6.27.8) answers with a card-present report whose status distinguishes present from not present (Table 73). Send it during a probe, once when transparent mode comes up and once per mode-1 dwell after that, so a removal is noticed within one scan cycle. Nothing in PDCAP says whether a reader implements it, and one that does not refuses in whatever way it likes, so treat any answer other than a card-present report as a verdict: latch it and go back to poll-only for the rest of the connection. Silence is not a verdict -- that is the link failing, and it takes the usual session error path. Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
The presence statuses started at 1, leaving 0 unnamed. A card-present event that never had a status decoded into it -- a zeroed struct, or an error path that returned before the status byte was read -- therefore held a value that is neither "present" nor "not present", and testing for absence as "!= OSDP_TRS_CARD_NOT_PRESENT" read it as a real sighting. Name it, and say in the header that presence must be tested against the named values. The existing values keep their numbering: renumbering them would silently change the meaning of an already-compiled application. Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
Now that the scan asks on a cadence, a card left sitting in the field answers every probe the same way, and each answer was delivered to the application as a fresh event. That is a stream of notifications saying nothing has changed. Report only what changed. Reports the reader volunteered on a poll are left alone: those are the reader's to repeat, and an application that missed the first one still needs to see the next. Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
A reader that refuses the mode-01 card scan leaves the presence scan unable to ask whether a card is still there, so removal is never reported. That is a permanent property of the reader and it changes what an application has to do -- find out for itself by sending a card command and reading its failure as the card being gone -- but nothing told it so. Say it on the notification that already carries presence-scan news, once per connection, for readers that refuse. Nothing in PDCAP covers this, so it cannot go through the capability report: the smart-card capability advertises transparent mode itself and has no bit for the individual mode-01 commands. Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
OSDP_TRS_APDU_MAX_LEN sizes the buffer in struct osdp_trs_apdu, but what fits on the wire is bounded by the packet size in use and the framing the APDU travels inside, which at the default packet size is appreciably less. An application had no way to ask, so sizing payloads against the constant -- the only number the header offered -- produced commands the CP turned away at submit time. Report the real limit. Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
A send-APDU command was checked against the packet size when the application submitted it, but a secure PIN entry carrying the same APDU was not. Its APDU sits behind the PIN parameter block, so it has less room, not more -- and an oversized one was only caught when the frame was built, part way through a running band, where the application can no longer do anything about it. Check both, against the room each command actually leaves. Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
Enabling the presence scan on a PD that has no smart-card reader is a configuration mistake worth reporting, and the check for it only ran if the PD happened to be online at that moment. Applications enable the scan once during setup, before any PD has been brought up, so it never ran for anyone. Check when the capability report actually arrives, and again on every reconnect that re-collects it, keeping it to one warning per enable. Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
Transparent mode is the one piece of state a PD carries between commands, and nothing dropped it when the CP restarted communication. A PD that was left in mode 01 stays there, which is the same "reader stops reporting ordinary card reads" condition the CP spends a round trip avoiding on its own side -- reproduced by our own PD role. Drop it on the sequence-number restart, and correct the comment there that said the PD holds no state between commands. Deliberately not on the PD's offline timeout: that measures silence, and a CP working through a slow card transaction is silent for the same reason it is still mid-session. Resetting there ends a session that is merely slow, which the emulated-card busy test catches. Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
The defaults were picked to make the scan responsive without field data behind them. Running it on real hardware showed the cost lands on ordinary credential reads: at 38400 baud a 75 ms default-mode dwell made a reader miss card reads outright, and 200 ms was the value that worked. Take the value that worked, and say in the header what makes it hard to choose -- that shortening the default-mode dwell makes the scan more aggressive rather than less, and that how short is too short depends on the baud rate, because a probe costs round trips rather than time. Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
Teach both PD fixtures to answer the mode-1 card scan -- the emulated card already knows whether it is in the field, so it answers from that -- and extend the wire-NAK hook to refuse or swallow only the card scan, leaving every other transparent command alone. The refusal and the silence cases must run in that order: a refusal latches "no card scan" for the rest of the connection, so silence, which must leave the verdict open, has to be proven while a verdict is still reachable. Signed-off-by: Siddharth Chandrasekaran <sidcha.dev@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TRS tunnels raw smart-card APDUs from the CP, through a PD acting as a transparent pipe, to a card in the reader (OSDP CMD_XWR/REPLY_XRD, capability SMART_CARD_SUPPORT). The control panel drives the card directly, so a compromised reader cannot forge a credential.
The CP runs the session as a library-driven state machine: it negotiates transparent mode, streams the app's APDUs, and tears the session down on osdp_cp_trs_stop(). Apps submit OSDP_CMD_XWR commands and receive card responses as OSDP_EVENT_TRS events.
The PD answers each APDU either synchronously, in the command callback, or deferred: it ACKs "working" and delivers the response on a later poll once a slow card responds. Deferral keeps a shared multi-drop bus non-blocking, so each PD's session makes progress independently.
Gated behind OPT_BUILD_OSDP_TRS (default off); wired into both build systems and covered by a CP<->PD APDU round-trip unit test.
Co-developed-by: Aaron Tulino (Aaronjamt) git@aaronjamt.com